/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2007 Ben Motmans <ben.motmans@gmail.com>
 * Copyright (C) 2007 Paul Burton <paulburton89@gmail.com>
 * Copyright (C) 2008 Philippe Durand <draekz@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 * 
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;

using Anculus.Core;

using Galaxium.Core;
using Galaxium.Protocol;

namespace Galaxium.Protocol.Msn
{
	public delegate void CommandHandler<T> (T cmd) where T : IMsnCommand;
	public delegate void CommandHandler (IMsnCommand cmd);
	
	public delegate void ContentHandler<T> (T content) where T : IMsnContent;
	public delegate void ContentHandler (IMsnContent content);
	
	public class CommandConnection : IDisposable
	{
		public event EventHandler<ConnectionEventArgs> Closed;
		public event EventHandler<ConnectionEventArgs> Established;
		public event EventHandler<ConnectionErrorEventArgs> ErrorOccurred;
		
		protected IConnection _connection;
		protected bool _reconnecting;
		
		protected const int _msgPayloadMax = 1400;
		
		// The maximum number of commands (that expect responses) that we'll
		// send before stopping and waiting for responses
		protected const int _maxWaiting = 1;
		
		protected byte[] _buffer = new byte[0];
		protected int _trid = 0;
		protected MsnConnectionType _type;
		
		protected static int _nsCount;
		protected static int _sbCount;
		protected int _dbgId;

		protected Dictionary<Type, List<Delegate>> _commandDelegates = new Dictionary<Type, List<Delegate>> ();
		protected Dictionary<Type, List<Delegate>> _contentDelegates = new Dictionary<Type, List<Delegate>> ();
		
		protected List<IMsnCommand> _outQueue = new List<IMsnCommand> ();
		protected Dictionary<int, IMsnCommand> _waitingResponse = new Dictionary<int, IMsnCommand> ();
		protected Dictionary<IMsnCommand, CommandHandler> _commandQueuedHandlers = new Dictionary<IMsnCommand, CommandHandler> ();
		protected Dictionary<int, CommandHandler> _commandResponseHandlers = new Dictionary<int, CommandHandler> ();
		
		protected static readonly byte _cr = (byte)13;
		protected static readonly byte _lf = (byte)10;
		protected static readonly byte _space = (byte)32;
		
		public MsnSession Session
		{
			get { return _connection.Session as MsnSession; }
		}
		
		public IConnection Connection
		{
			get { return _connection; }
		}
		
		public bool Reconnecting
		{
			get { return _reconnecting; }
		}
		
		public virtual bool CanSend
		{
			get { return _connection.IsConnected && (!_connection.IsSending) && (_waitingResponse.Count <= _maxWaiting); }
		}
		
		public List<IMsnCommand> OutQueue
		{
			get { return _outQueue; }
		}
		
		public CommandConnection (MsnSession session, IConnectionInfo info, MsnConnectionType type)
		{
			if (!info.UseHTTP)
			{
				_connection = new TCPConnection (session, info);
			}
			else
			{
				_connection = new HTTPConnection (session, info);
				
				if (type == MsnConnectionType.Notification)
				{
					info.HostName = "gateway.messenger.hotmail.com";
					((HTTPConnection)_connection).IP = "messenger.hotmail.com";
					((HTTPConnection)_connection).Type = HTTPServerTypes.NS;
				}
				else if (type == MsnConnectionType.Switchboard)
				{
					((HTTPConnection)_connection).IP = info.HostName;
					((HTTPConnection)_connection).Type = HTTPServerTypes.SB;
				}
			}
			
			if (type == MsnConnectionType.Notification)
				_dbgId = ++_nsCount;
			else if (type == MsnConnectionType.Switchboard)
				_dbgId = ++_sbCount;
			
			_connection.Closed += OnClosed;
			_connection.Established += OnEstablished;
			_connection.ErrorOccurred += OnErrorOccurred;
			_connection.DataReceived += OnDataReceived;
			_connection.AfterConnect += OnAfterConnect;
			_connection.SendComplete += OnSendComplete;
			
			_type = type;
			
			ClearHandlers ();
			AutoAddHandlers ();
		}
		
		public void Connect ()
		{
			_connection.Connect ();
		}
		
		public void Disconnect ()
		{
			_connection.Disconnect ();
		}
		
		public void Reconnect ()
		{
			_reconnecting = true;
			_connection.Reconnect ();
		}
		
		public virtual void Dispose ()
		{
			_connection.Dispose ();
		}
		
		protected virtual void OnEstablished (object sender, ConnectionEventArgs args)
		{
			if (_reconnecting)
			{
				_reconnecting = false;
				return;
			}
			
			if (Established != null)
				Established (sender, args);
		}
		
		protected virtual void OnAfterConnect (object sender, ConnectionEventArgs args)
		{
			_trid = 0;
		}
		
		protected virtual void OnSendComplete (object sender, EventArgs args)
		{
			ProcessOutQueue ();
		}
		
		protected virtual void OnReady (object sender, EventArgs args)
		{
			
		}
		
		protected virtual void OnClosed (object sender, ConnectionEventArgs args)
		{
			if (!_reconnecting)
			{
				Log.Debug ("Firing closed event.");
				
				if (Closed != null)
					Closed (sender, args);
			}
			
			_outQueue.Clear ();
			_waitingResponse.Clear ();
			_commandQueuedHandlers.Clear ();
			_commandResponseHandlers.Clear ();
		}
		
		protected virtual void OnErrorOccurred (object sender, ConnectionErrorEventArgs args)
		{
			if (ErrorOccurred != null)
				ErrorOccurred (sender, args);
		}
		
		protected virtual void OnDataReceived (object sender, ConnectionDataEventArgs args)
		{
			ThreadUtility.Check ();
			
			//lock (_buffer)
			{
				AppendToBuffer (args.Buffer, args.Length);
			
				while (true)
				{	
					int iCrlf = GetCrlfIndex ();
					
					if (iCrlf < 0)
						return;
					
					string cmdName = Encoding.UTF8.GetString (_buffer, 0, 3);
					Type cmdType = MsnCommandAttribute.FindType (cmdName, _type);
					
					if (cmdType == null)
						cmdType = typeof (ErrorCommand);
					
					byte[] cmdData = null;
					
					MsnCommandAttribute commandAtt = MsnCommandAttribute.Find (cmdType);
					PayloadCommandAttribute payloadAtt = PayloadCommandAttribute.Find (cmdType);
					
					if ((payloadAtt != null) && payloadAtt.IsIn)
					{
						int size = GetPayloadSize (iCrlf);
						
						if (size < 0)
							return;
						
						int required = iCrlf + 2 + size;
						
						// Check we have received the whole command
						if (required > _buffer.Length)
							return;
						
						cmdData = ReadBuffer (required);
						ShrinkBuffer (required);
					}
					else
					{
						cmdData = ReadBuffer (iCrlf);
						ShrinkBuffer (iCrlf + 2);
					}
					
					IMsnCommand cmd = null;
					
					try
					{
						if (commandAtt.Parser != null)
							cmd = commandAtt.Parser.Parse ((MsnSession)Session, cmdData);
						else
							cmd = (IMsnCommand)Activator.CreateInstance (cmdType, Session, cmdData);
					}
					catch (Exception ex)
					{
						Log.Error (ex, "Error parsing command!");
						
						try
						{
							Log.Debug (Encoding.UTF8.GetString (cmdData));
						}
						catch
						{
							Log.Warn ("Unable to decode cmdData");
							
							string dataStr = string.Empty;
							foreach (byte b in cmdData)
								dataStr += string.Format ("{0:x2} ", b);
							
							Log.Warn (dataStr);
						}
						
						cmd = null;
					}
					
					if (cmd != null)
					{
						List<Delegate> delegates = new List<Delegate> ();
						
						if (!_commandDelegates.ContainsKey (cmd.GetType ()))
						{
							if (cmd is ContentCommand)
								delegates.AddRange (_commandDelegates[typeof (ContentCommand)]);
							
							if (delegates.Count == 0)
								Log.Warn ("Unexpected command ({0}):", cmd.GetType ().FullName);
						}
						else
							delegates.AddRange (_commandDelegates[cmd.GetType ()]);
						
						if (cmd.IsTransIn)
						{
							if (_commandResponseHandlers.ContainsKey (cmd.TransactionID))
							{
								delegates.Add (_commandResponseHandlers[cmd.TransactionID]);
								_commandResponseHandlers.Remove (cmd.TransactionID);
							}
							
							if (_waitingResponse.ContainsKey (cmd.TransactionID))
								_waitingResponse.Remove (cmd.TransactionID);
						}
						
						Log.Info ("<< {0} {1} {2} [{3}]: {4}", _type == MsnConnectionType.Notification ? "NS" : "SB", _dbgId, _connection.ConnectionInfo.UseHTTP ? "HTTP" : "TCP", _connection.ConnectionInfo.HostName, cmd);
						
						foreach (Delegate del in delegates)
						{
							try
							{
								del.DynamicInvoke (cmd);
							}
							catch (Exception ex)
							{
								Log.Error (ex, "Error Invoking {0} Handler {1}.{2}", cmd.GetType ().Name, del.Method.DeclaringType.Name, del.Method.Name);
							}
						}
						
						// If we've received a response for a command, we might now be ready to send
						ProcessOutQueue ();
					}
				}
			}
		}
		
		public virtual int Send (IMsnCommand cmd, CommandHandler handler, bool force)
		{
			ThrowUtility.ThrowIfNull ("cmd", cmd);
			
			if (handler != null)
				_commandQueuedHandlers.Add (cmd, handler);
			
			if (force)
				TrueSend (cmd);
			else
			{
				_outQueue.Add (cmd);
				ProcessOutQueue ();
			}
			
			return _trid;
		}
		
		public virtual int Send (IMsnCommand cmd, CommandHandler handler)
		{
			return Send (cmd, handler, false);
		}
		
		public virtual int Send (IMsnCommand cmd)
		{
			return Send (cmd, null);
		}
		
		public virtual int Send (IMsnCommand cmd, bool force)
		{
			return Send (cmd, null, force);
		}
		
		void TrueSend (IMsnCommand cmd)
		{
			if (cmd.IsTransOut)
			{
				cmd.TransactionID = ++_trid;
				
				if (_commandQueuedHandlers.ContainsKey (cmd))
				{
					CommandHandler handler = _commandQueuedHandlers[cmd];
					_commandQueuedHandlers.Remove (cmd);
					_commandResponseHandlers[cmd.TransactionID] = handler;
					
					//Log.Debug ("Queued {0} response handler {1}.{2} assigned TransID {3}", cmd.Command, (handler as Delegate).Method.DeclaringType.Name, (handler as Delegate).Method.Name, cmd.TransactionID);
				}
				//else
				//	Log.Debug ("No queued {0} response handler", cmd.Command);
				
				if (cmd.ExpectResponse)
					_waitingResponse[cmd.TransactionID] = cmd;
			}
			
			Log.Info (">> {0} {1} {2} [{3}]: {4}", _type == MsnConnectionType.Notification ? "NS" : "SB", _dbgId, _connection.ConnectionInfo.UseHTTP ? "HTTP" : "TCP", _connection.ConnectionInfo.HostName, cmd);
			
			// If we are using HTTP method, we may need to setup some stuff
			if (Connection.ConnectionInfo.UseHTTP)
			{
				HTTPConnection httpConnection = (HTTPConnection)Connection;
				
				if (cmd is VERCommand || cmd is SBUSRCommand || cmd is ANSCommand)
					httpConnection.Action = HTTPActions.Open;
				else
					httpConnection.Action = HTTPActions.None;
			}
			
			_connection.Send (cmd.ToByteArray ());
		}
		
		protected virtual void ProcessOutQueue ()
		{
			// We aren't ready to send right now
			if (!CanSend)
				return;
			
			//Log.Debug ("{0} Messages Queued", _outQueue.Count);
			
			if (_outQueue.Count == 0)
			{
				// We're ready, we've got nothing to send, something might want to know
				OnReady (this, EventArgs.Empty);
				return;
			}
			
			IMsnCommand cmd = _outQueue[0];
			_outQueue.RemoveAt (0);
			
			TrueSend (cmd);
		}
		
		protected void SendCleanDisconnect ()
		{
			Send (new OUTCommand (Session as MsnSession));
		}
		
		private int GetPayloadSize (int crlf)
		{
			for (int i = crlf; i > 0; i--)
			{
				byte b = _buffer[i];

				if (b == _space)
				{
					int len = crlf - i - 1;
					byte[] sizeBuffer = new byte[len];
					Array.Copy (_buffer, i + 1, sizeBuffer, 0, len);

					int payload = 0;
					int.TryParse (Encoding.UTF8.GetString (sizeBuffer), out payload);

					return payload;
				}
			}

			return -1;
		}

		private byte[] ReadBuffer (int size)
		{
			byte[] data = new byte[size];
			Array.Copy (_buffer, 0, data, 0, size);
			return data;
		}

		private void ShrinkBuffer (int skip)
		{
			if (skip > _buffer.Length)
				skip = _buffer.Length;
			
			byte[] newBuffer = new byte[_buffer.Length - skip];

			if (newBuffer.Length > 0)
				Array.Copy (_buffer, skip, newBuffer, 0, newBuffer.Length);

			_buffer = newBuffer;
		}

		private int GetCrlfIndex ()
		{
			byte prev = 0;
			
			for (int i = 3 /*skip the command*/; i < _buffer.Length; i++)
			{
				byte b = _buffer[i];

				if (b == _lf && prev == _cr)
					return i - 1;

				prev = b;
			}

			return -1;
		}

		private void AppendToBuffer (byte[] data, int length)
		{
			byte[] newBuffer = new byte[_buffer.Length + length];

			Array.Copy (_buffer, 0, newBuffer, 0, _buffer.Length);
			Array.Copy (data, 0, newBuffer, _buffer.Length, length);

			_buffer = newBuffer;
		}
		
		[CommandHandler]
		protected virtual void OnContentReceived (ContentCommand cmd)
		{
			IMsnContent content = cmd.Content;
			
			if (content == null)
			{
				Log.Warn ("Unknown Content Type {0}", cmd.ContentType);
				return;
			}
			
			if (!_contentDelegates.ContainsKey (content.GetType ()))
			{
				Log.Warn ("Unexpected Content {0}", content);
				return;
			}
			
			// Make a local copy of the list in case any get removed by a handler
			List<Delegate> tmp = new List<Delegate> ();
			tmp.AddRange (_contentDelegates[content.GetType ()]);
			
			foreach (Delegate del in tmp)
			{
				try
				{
					del.DynamicInvoke (content);
				}
				catch (Exception ex)
				{
					Log.Error (ex, "Error Invoking {0} Handler {1}.{2}", content.GetType ().Name, del.Method.DeclaringType.Name, del.Method.Name);
				}
			}
		}
		
		public void AddCommandHandler (Type cmdType, Delegate handler)
		{
			if (!_commandDelegates.ContainsKey (cmdType))
				_commandDelegates.Add (cmdType, new List<Delegate> ());
			
			ThrowUtility.ThrowIfTrue ("Handler already present", _commandDelegates[cmdType].Contains (handler));
			
			_commandDelegates[cmdType].Add (handler);
		}

		public void AddCommandHandler<T> (CommandHandler<T> handler) where T : IMsnCommand
		{
			AddCommandHandler (typeof (T), handler);
		}
		
		public void RemoveCommandHandler (Type cmdType, Delegate handler)
		{
			ThrowUtility.ThrowIfFalse ("Handler not found", _commandDelegates.ContainsKey (cmdType));
			ThrowUtility.ThrowIfFalse ("Handler not found", _commandDelegates[cmdType].Contains (handler));

			_commandDelegates[cmdType].Remove (handler);
			
			if (_commandDelegates[cmdType].Count == 0)
				_commandDelegates.Remove (cmdType);
		}
		
		public void RemoveCommandHandler<T> (CommandHandler<T> handler) where T : IMsnCommand
		{
			RemoveCommandHandler (typeof (T), handler);
		}
		
		public void AddContentHandler (Type contentType, Delegate handler)
		{
			if (!_contentDelegates.ContainsKey (contentType))
				_contentDelegates.Add (contentType, new List<Delegate> ());
			
			ThrowUtility.ThrowIfTrue ("Handler already present", _contentDelegates[contentType].Contains (handler));

			_contentDelegates[contentType].Add (handler);
		}

		public void AddContentHandler<T> (ContentHandler<T> handler) where T : IMsnContent
		{
			AddContentHandler (typeof (T), handler);
		}
		
		public void RemoveContentHandler (Type contentType, Delegate handler)
		{
			ThrowUtility.ThrowIfFalse ("Handler not found", _contentDelegates.ContainsKey (contentType));
			ThrowUtility.ThrowIfFalse ("Handler not found", _contentDelegates[contentType].Contains (handler));

			_contentDelegates[contentType].Remove (handler);
			
			if (_contentDelegates[contentType].Count == 0)
				_contentDelegates.Remove (contentType);
		}
		
		public void RemoveContentHandler<T> (ContentHandler<T> handler) where T : IMsnContent
		{
			RemoveContentHandler (typeof (T), handler);
		}
		
		public void ClearHandlers ()
		{
			_commandDelegates.Clear ();
			_contentDelegates.Clear ();
		}
		
		public void AutoAddHandlers ()
		{
			foreach (MethodInfo method in this.GetType ().GetMethods (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
			{
				if (method.GetCustomAttributes (typeof (CommandHandlerAttribute), false).Length > 0)
				{
					Type cmdType = null;
					try {
						cmdType = method.GetParameters ()[0].ParameterType;
						AddCommandHandler (cmdType, CreateDelegate (method, cmdType, typeof (CommandHandler)));
					} catch (Exception e) {
						Log.Error (e, "Unable to create command handler (obj={0}, method={1}, type={2}).",
							this, method.Name, cmdType == null ? "null" : cmdType.FullName);
					}
				}
				
				if (method.GetCustomAttributes (typeof (ContentHandlerAttribute), false).Length > 0)
				{
					Type contentType = null;
					try {
						contentType = method.GetParameters ()[0].ParameterType;
						AddContentHandler (contentType, CreateDelegate (method, contentType, typeof (ContentHandler)));
					} catch (Exception e) {
						Log.Error (e, "Unable to create content handler (obj={0}, method={1}, type={2}).",
							this, method.Name, contentType == null ? "null" : contentType.FullName);
					}
				}
			}
		}
		
		private Delegate CreateDelegate (MethodInfo method, Type paramType, Type delegateType)
		{
			try
			{
				DynamicMethod dynm = new DynamicMethod (String.Empty, null,
				                                        new Type[] { typeof (object), typeof (object) },
														method.DeclaringType);
				
				ILGenerator ilgen = dynm.GetILGenerator ();
				
				ilgen.Emit (OpCodes.Ldarg_0);
				ilgen.Emit (OpCodes.Castclass, method.DeclaringType);
				ilgen.Emit (OpCodes.Ldarg_1);
				ilgen.Emit (OpCodes.Castclass, paramType);
				
				if (method.IsFinal)
					ilgen.EmitCall (OpCodes.Call, method, null);
				else
					ilgen.EmitCall (OpCodes.Callvirt, method, null);
				
				ilgen.Emit (OpCodes.Ret);
				
				return dynm.CreateDelegate (delegateType, this);
			}
			catch (InvalidProgramException)
			{
				// This happens on mono <= 1.2.4
				
				return Delegate.CreateDelegate (delegateType, this, method);
			}
		}
	}
}
